package org.ecbox.android.sdk.internal.util.json;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.CharacterIterator;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.StringCharacterIterator;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import java.util.TimeZone;

import org.ecbox.android.sdk.Constants;

import com.googlecode.openbeans.BeanInfo;
import com.googlecode.openbeans.IntrospectionException;
import com.googlecode.openbeans.Introspector;
import com.googlecode.openbeans.PropertyDescriptor;

public class JSONWriter {

	private StringBuffer buf = new StringBuffer();
	private Stack<Object> calls = new Stack<Object>();
	private boolean emitClassName = true;
	private DateFormat format;

	public JSONWriter(boolean emitClassName) {
		this.emitClassName = emitClassName;
	}

	public JSONWriter() {
		this(false);
	}

	public JSONWriter(DateFormat format) {
		this(false);
		this.format = format;
	}

	public String write(Object object) {
		buf.setLength(0);
		value(object);
		return buf.toString();
	}

	public String write(long n) {
		return String.valueOf(n);
	}

	public String write(double d) {
		return String.valueOf(d);
	}

	public String write(char c) {
		return "\"" + c + "\"";
	}

	public String write(boolean b) {
		return String.valueOf(b);
	}

	private void value(Object object) {
		if (object == null || cyclic(object)) {
			add(null);
		} else {
			calls.push(object);
			if (object instanceof Class<?>)
				string(object);
			else if (object instanceof Boolean)
				bool(((Boolean) object).booleanValue());
			else if (object instanceof Number)
				add(object);
			else if (object instanceof String)
				string(object);
			else if (object instanceof Character)
				string(object);
			else if (object instanceof Map<?, ?>)
				map((Map<?, ?>) object);
			else if (object.getClass().isArray())
				array(object);
			else if (object instanceof Iterator<?>)
				array((Iterator<?>) object);
			else if (object instanceof Collection<?>)
				array(((Collection<?>) object).iterator());
			else if (object instanceof Date)
				date((Date) object);
			else
				bean(object);
			calls.pop();
		}
	}

	private boolean cyclic(Object object) {
		Iterator<Object> it = calls.iterator();
		while (it.hasNext()) {
			Object called = it.next();
			if (object == called)
				return true;
		}
		return false;
	}

	private void bean(Object object) {
		add("{");
		BeanInfo info;
		boolean addedSomething = false;
		try {
			info = Introspector.getBeanInfo(object.getClass());
			PropertyDescriptor[] props = info.getPropertyDescriptors();
			for (int i = 0; i < props.length; ++i) {
				PropertyDescriptor prop = props[i];
				String name = prop.getName();
				Method accessor = prop.getReadMethod();
				if ((emitClassName || !"class".equals(name)) && accessor != null) {
					if (!accessor.isAccessible())
						accessor.setAccessible(true);
					Object value = accessor.invoke(object, (Object[]) null);
					if (value == null)
						continue;
					if (addedSomething)
						add(',');
					add(name, value);
					addedSomething = true;
				}
			}
			Field[] ff = object.getClass().getFields();
			for (int i = 0; i < ff.length; ++i) {
				Field field = ff[i];
				Object value = field.get(object);
				if (value == null)
					continue;
				if (addedSomething)
					add(',');
				add(field.getName(), value);
				addedSomething = true;
			}
		} catch (IllegalAccessException iae) {
			iae.printStackTrace();
		} catch (InvocationTargetException ite) {
			ite.getCause().printStackTrace();
			ite.printStackTrace();
		} catch (IntrospectionException ie) {
			ie.printStackTrace();
		}
		add("}");
	}

	private void add(String name, Object value) {
		add('"');
		add(name);
		add("\":");
		value(value);
	}

	private void map(Map<?, ?> map) {
		add("{");
		Iterator<?> it = map.entrySet().iterator();
		while (it.hasNext()) {
			Map.Entry<?, ?> e = (Map.Entry<?, ?>) it.next();
			value(e.getKey());
			add(":");
			value(e.getValue());
			if (it.hasNext())
				add(',');
		}
		add("}");
	}

	private void array(Iterator<?> it) {
		add("[");
		while (it.hasNext()) {
			value(it.next());
			if (it.hasNext())
				add(",");
		}
		add("]");
	}

	private void array(Object object) {
		add("[");
		int length = Array.getLength(object);
		for (int i = 0; i < length; ++i) {
			value(Array.get(object, i));
			if (i < length - 1)
				add(',');
		}
		add("]");
	}

	private void bool(boolean b) {
		add(b ? "true" : "false");
	}

	private void date(Date date) {
		if (this.format == null) {
			this.format = new SimpleDateFormat(Constants.DATE_TIME_FORMAT, Locale.US);
			this.format.setTimeZone(TimeZone.getTimeZone(Constants.DATE_TIMEZONE));
		}
		add("\"");
		add(format.format(date));
		add("\"");
	}

	private void string(Object obj) {
		add('"');
		CharacterIterator it = new StringCharacterIterator(obj.toString());
		for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
			if (c == '"')
				add("\\\"");
			else if (c == '\\')
				add("\\\\");
			else if (c == '/')
				add("\\/");
			else if (c == '\b')
				add("\\b");
			else if (c == '\f')
				add("\\f");
			else if (c == '\n')
				add("\\n");
			else if (c == '\r')
				add("\\r");
			else if (c == '\t')
				add("\\t");
			else if (Character.isISOControl(c)) {
				unicode(c);
			} else {
				add(c);
			}
		}
		add('"');
	}

	private void add(Object obj) {
		buf.append(obj);
	}

	private void add(char c) {
		buf.append(c);
	}

	static char[] hex = "0123456789ABCDEF".toCharArray();

	private void unicode(char c) {
		add("\\u");
		int n = c;
		for (int i = 0; i < 4; ++i) {
			int digit = (n & 0xf000) >> 12;
			add(hex[digit]);
			n <<= 4;
		}
	}
}
